import {Promise} from 'es6-promise';
import { ErrCode } from './error.code';
import {Collection, Cursor, AggregationCursor} from "mongodb";

import {IModel} from "./types";
import * as utils from './utils';
import {
    FindParam, CountParam, GroupParam, DistinctParam, MapReduceParam, AggregateParam,
    DeleteParam, UpdateParam, InsertParam, FindOneReplaceParam, FindOneDeleteParam,
    FindOneUpdateParam, FindOneParam, FindAndDeleteOptions, FindAndUpdateOptions, Param,
} from "./params";

export class BaseModel implements IModel {
    private _colletion: Collection;

    constructor(coll: Collection) {
        this._colletion = coll;
    }

    get collection(): Collection { return this._colletion; }

    name(): string { return 'BaseModel'; }
    toString(): string { return this.name(); }
    
    find(param: FindParam): Promise<any> {
        return this._checkParams(param).then(() => {
            let cursor: Cursor =  this._colletion.find(param.filter);
            if(param.sort && utils.isNotEmpty(param.sort)){
                cursor = cursor.sort(param.sort);
            }

            if(param.skip > 0) {
                cursor = cursor.skip(param.skip);
            }

            if(param.limit > 0) {
                cursor = cursor.limit(param.limit);
            }

            if(utils.isNotEmpty(param.project)){
                cursor = cursor.project(param.project);
            }
            
            return cursor.toArray();
        });
    };

    findOne(param: FindOneParam): Promise<any> {
        return this._checkParams(param).then(() => {
            let cursor: Cursor =  this._colletion.find(param.filter);
            if(param.sort && utils.isNotEmpty(param.sort)){
                cursor = cursor.sort(param);
            }

            cursor = cursor.limit(1);
            if(utils.isNotEmpty(param.project)) {
                cursor = cursor.project(param.project);
            }

            return cursor.next();
        });
    };

    findOneAndUpdate(param: FindOneUpdateParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            let options: {project?: Object, sort?: Object, upsert?: boolean} = {};
            if(utils.isNotEmpty(param.project)){
                options.project = param.project;
            }

            if(utils.isNotEmpty(param.sort)){
                options.sort = param.sort;
            }

            if(param.upsert){
                options.upsert = true;
            }
            return this._colletion.findOneAndUpdate(param.filter, param.update, options);
        });
    };

    findOneAndDelete(param: FindOneDeleteParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            let options: FindAndDeleteOptions = {};
            if(utils.isNotEmpty(param.project)){
                options.project = param.project;
            }

            if(utils.isNotEmpty(param.sort)){
                options.sort = param.sort;
            }

            return this._colletion.findOneAndUpdate(param.filter, options);
        });
    };

    findOneAndReplace(param: FindOneReplaceParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            let options: FindAndUpdateOptions = {};
            if(utils.isNotEmpty(param.project)){
                options.project = param.project;
            }

            if(utils.isNotEmpty(param.sort)){
                options.sort = param.sort;
            }

            if(param.upsert){
                options.upsert = param.upsert;
            }

            return this._colletion.findOneAndUpdate(param.filter, options);
        });
    };

    insertOne(param: InsertParam<Object>): Promise<any> {
        return this._checkParams(param).then( ()=>{
            if(utils.isArray(param.doc)){
                throw new Error(`${this.name()}: insertOne doc parameter must be an object!`);
            }
            return this._colletion.insertOne(param.doc, param.options).then((ret) => {
                if( !(ret && ret.result && ret.result.ok) ){
                    return Promise.reject({err_code: ErrCode.ServerError});
                }
                return Promise.resolve(ret);
            });
        });
    };

    insertMany(param: InsertParam<Object[]>): Promise<any> {
        return this._checkParams(param).then( ()=>{
            if(!utils.isArray(param.doc)){
                throw new Error(`${this.name()}: insertMany doc parameter must be an object array!`);
            }
            return this._colletion.insertMany(param.doc, param.options).then((ret) => {
                if( !(ret && ret.result && ret.result.ok) ){
                    return Promise.reject({err_code: ErrCode.ServerError});
                }
                return Promise.resolve(ret);
            });
        });
    };

    updateOne(param: UpdateParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            if(!utils.isObject(param.update)){
                throw new Error(`${this.name()}: updateOne update parameter must be an!`);
            }
            return this._colletion.updateOne(param.filter, param.update, param.options).then((ret) => {
                if( !(ret && ret.result && ret.result.ok) ){
                    return Promise.reject({err_code: ErrCode.ServerError});
                }
                return Promise.resolve(ret);
            });
        });
    };

    updateMany(param: UpdateParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            if(!utils.isObject(param.update)){
                throw new Error(`${this.name()}: updateOne update parameter must be an object!`);
            }
            return this._colletion.updateMany(param.filter, param.update, param.options).then((ret) => {
                if( !(ret && ret.result && ret.result.ok) ){
                    return Promise.reject({err_code: ErrCode.ServerError});
                }
                return Promise.resolve(ret);
            });
        });
    };

    deleteOne(param: DeleteParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            return this._colletion.deleteOne(param.filter, param.options).then((ret) => {
                if( !(ret && ret.result && ret.result.ok) ){
                    return Promise.reject({err_code: ErrCode.ServerError});
                }
                return Promise.resolve(ret);
            });
        });
    };

    deleteMany(param: DeleteParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            return this._colletion.deleteMany(param.filter, param.options).then((ret) => {
                if( !(ret && ret.result && ret.result.ok) ){
                    return Promise.reject({err_code: ErrCode.ServerError});
                }
                return Promise.resolve(ret);
            });
        });
    };

    count(param: CountParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            return this._colletion.count(param.filter, {});
        });
    };

    group(param: GroupParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            return this._colletion.group(
                param.keys,
                param.condition || {},
                param.initial || {},
                param.reduce || utils.nop,
                param.finalize || utils.nop,
                param.command || false
            );
        });
    };

    distinct(param: DistinctParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            return this._colletion.distinct(param.key, param.filter || {});
        });
    };

    mapReduce(param: MapReduceParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            return this._colletion.mapReduce(param.map, param.reduce, param.options);
        });
    };

    aggregate(param: AggregateParam): Promise<any> {
        return this._checkParams(param).then( ()=>{
            let cursor: AggregationCursor = this._colletion.aggregate(param.pipeline);
            return cursor.toArray();
        });
    };

    protected _checkParams(param: Param): Promise<any> {
        return new Promise<any>((resolve: any, reject: any) => {
            if( utils.isUndefined(this._colletion) || utils.isNull(this._colletion) ) {
                reject( new Error(`${this.name()}: collection instance is not accessable.`) );
                return;
            }

            if(!param.isValid()){
                reject( Error(`${this.name()}: param validation failed!`) );
                return;
            }
            resolve();
        });
    }
}